package com.gframework.boot.flyway;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.flywaydb.core.Flyway;
import org.flywaydb.core.api.FlywayException;
import org.flywaydb.core.api.configuration.FluentConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration.FlywayConfiguration;
import org.springframework.boot.autoconfigure.flyway.FlywayProperties;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.lang.Nullable;
import org.springframework.objenesis.Objenesis;
import org.springframework.objenesis.ObjenesisStd;
import org.springframework.util.StringUtils;

import com.gframework.util.GUtils;
import com.gframework.util.ReflectUtils;

/**
 * flyway扩展启动类.
 * <p>
 * 此类是为了解决flyway优先执行的问题（由于部分环境配置依赖数据库，而数据库由依赖flyway自动加载，而flyway又需要环境配置才能够实例化。
 * 正是因为有这样一个矛盾的闭环问题，所以才设计此类）
 * <p>
 * 同时也是为了解决flyway与druid防火墙冲突问题，flyway使用最基本的jdbc数据源就可以了。不需要第三方数据库连接池。
 * <p>
 * 本类会在默认资源环境加载完毕后紧接着执行。
 * 
 * @since 1.0.0
 * @author Ghwolf
 */
class FlywayExecute {

	private static final Logger logger = LoggerFactory.getLogger(FlywayExecute.class);
	
	void doRunFlyway(FlywayProperties pro,ConfigurableEnvironment env) {
		
		if (pro.getUrl() == null) {
			throw new FlywayException("未指定flyway执行时的连接url，可以通过指定spring.flyway.url/spring.datasource.url配置项或实现一个IFlywayExtendSPI接口的子类并配置到spring.factories中以指定url参数信息！");
		}
		if (!StringUtils.startsWithIgnoreCase(pro.getUrl(), "jdbc:sqlite")) {
			if (pro.getUser() == null) {
				throw new FlywayException("未指定flyway执行时的user，可以通过指定spring.flyway.user/spring.datasource.username配置项或实现一个IFlywayExtendSPI接口的子类并配置到spring.factories中以指定user参数信息！");
			}
			if (pro.getPassword() == null) {
				throw new FlywayException("未指定flyway执行时的password，可以通过指定spring.flyway.password/spring.datasource.password配置项或实现一个IFlywayExtendSPI接口的子类并配置到spring.factories中以指定password参数信息！");
			}
		}
		
		List<IFlywayHookSPI> hooks = SpringFactoriesLoader.loadFactories(IFlywayHookSPI.class, Thread.currentThread().getContextClassLoader());
		
		hooks.forEach(c->c.beforeConfig(pro, env));

		FluentConfiguration conf = new FluentConfiguration();
		conf.dataSource(pro.getUrl(), pro.getUser(), pro.getPassword());
		try {
			if (ReflectUtils.hasField(FlywayConfiguration.class, "properties")) {
				// springboot2.1.0以前的版本
				Objenesis objenesis = new ObjenesisStd(false);
				FlywayConfiguration fc = objenesis.getInstantiatorOf(FlywayConfiguration.class).newInstance();
				Field propertiesField = FlywayConfiguration.class.getDeclaredField("properties");
				propertiesField.setAccessible(true);
				propertiesField.set(fc, pro);
				
				Method configurePropertiesMethod = FlywayConfiguration.class.getDeclaredMethod("configureProperties",FluentConfiguration.class);
				configurePropertiesMethod.setAccessible(true);
				configurePropertiesMethod.invoke(fc, conf);
			} else {
				// springboot2.2.0以后的版本
				Method configurePropertiesMethod = FlywayConfiguration.class.getDeclaredMethod("configureProperties",
						FluentConfiguration.class, FlywayProperties.class);
				configurePropertiesMethod.setAccessible(true);
				configurePropertiesMethod.invoke(new FlywayConfiguration(), conf, pro);
			}
			
		} catch (Exception e) {
			throw new UnsupportedOperationException("flyway初始化失败，可能由于flyway/springboot版本升级或降低导致不兼容情况出现！", e);
		}

		conf.placeholders(new FlywayPlaceholdersMap(conf.getPlaceholders(), env));

		if (logger.isInfoEnabled()) {
			logger.info("【------------------------------】");
			logger.info("【-----------flyway执行----------】");
			logger.info("【------------------------------】");
			if (logger.isDebugEnabled()) {
				logger.debug("【locations:{}】", pro.getLocations());
				logger.debug("【initSqls:{}】", pro.getInitSqls());
				logger.debug("【url:{}】", pro.getUrl());
			}
		}

		Flyway flyway = new Flyway(conf);

		hooks.forEach(c->c.beforeMigrate(pro, env));
		
		int migrateEnd = flyway.migrate();
		
		hooks.forEach(c->c.afterMigrate(pro, env));
		
		if (logger.isInfoEnabled()) {
			if (migrateEnd == 0) {
				logger.info("【-----flyway未执行，无sql变更-----】");
			} else {
				logger.info("【----------flyway执行完毕--------】");
			}
		}
	}


	class FlywayPlaceholdersMap implements Map<String, String> {

		private final Map<String, String> pro;
		private final Environment env;

		FlywayPlaceholdersMap(@Nullable Map<String, String> pro, Environment env) {
			this.pro = GUtils.nvl(pro, Collections.emptyMap());
			this.env = env;
		}

		@Override
		public int size() {
			return pro.size() + 1;
		}

		@Override
		public boolean isEmpty() {
			return false;
		}

		@Override
		public boolean containsKey(Object key) {
			if (key == null) {
				return false;
			}
			return pro.containsKey(key) || env.containsProperty(key.toString());
		}

		@Override
		public boolean containsValue(Object value) {
			return pro.containsValue(value);
		}

		@Override
		public String get(Object key) {
			if (key == null) return null;
			if (pro.containsKey(key)) {
				return pro.get(key);
			} else {
				return env.getProperty(key.toString());
			}
		}

		@Override
		public String put(String key, String value) {
			return null;
		}

		@Override
		public String remove(Object key) {
			return null;
		}

		@Override
		public void putAll(Map<? extends String, ? extends String> m) {
		}

		@Override
		public void clear() {
		}

		@Override
		public Set<String> keySet() {
			return pro.keySet();
		}

		@Override
		public Collection<String> values() {
			return pro.values();
		}

		@Override
		public Set<java.util.Map.Entry<String, String>> entrySet() {
			return pro.entrySet();
		}
	}

}
